package com.shortlink.server.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.shortlink.common.base.BasePageReqDTO;
import com.shortlink.common.exception.ClientException;
import com.shortlink.common.util.UrlUtils;
import com.shortlink.server.dao.entity.ShortLinkStatsRecordDO;
import com.shortlink.server.dto.shortlinkmapper.ShortLinkReqDTO;
import com.shortlink.server.dto.shortlinkstats.*;
import com.shortlink.server.service.ShortLinkStatsRecordService;
import com.shortlink.server.service.ShortLinkStatsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

/**
 * 短链接监控接口实现层
 *
 * @author wp
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkStatsServiceImpl implements ShortLinkStatsService {
    private static final String SHORT_LINK_NAME = "(domain, short_uri)";
    private final ShortLinkStatsRecordService shortLinkStatsRecordService;

    @Override
    public List<ShortLinkTodayBaseStatsRespDTO> batchGetShortLinkTodayBaseStats(List<ShortLinkReqDTO> shortLinks) {
        if (ObjectUtil.isEmpty(shortLinks)) {
            return new ArrayList<>();
        }
        shortLinks.forEach(shortLink -> verifyShortLink(shortLink.getDomain(), shortLink.getShortUri()));

        return shortLinkStatsRecordService.batchGetShortLinkTodayBaseStats(shortLinks);
    }

    @Override
    public ShortLinkStatsVO signalShortLinkStats(SignalShortLinkStatsReqDTO signalShortLinkStats) {
        String domain = signalShortLinkStats.getDomain();
        String shortUri = signalShortLinkStats.getShortUri();
        verifyShortLink(domain, shortUri);
        verifyDateSelector(signalShortLinkStats);

        LocalDate startDate = signalShortLinkStats.getStartDate();
        LocalDate endDate = signalShortLinkStats.getEndDate();
        List<ShortLinkStatsRecordDO> list = shortLinkStatsRecordService.lambdaQuery()
                .eq(ShortLinkStatsRecordDO::getDomain, domain)
                .eq(ShortLinkStatsRecordDO::getShortUri, shortUri)
                .between(ShortLinkStatsRecordDO::getVisitDate, startDate, endDate)
                .eq(ShortLinkStatsRecordDO::getDelFlag, ShortLinkStatsRecordDO.DEL_FLAG_FAIL)
                .list();

        return stats(list, startDate, endDate);
    }

    @Override
    public IPage<ShortLinkStatsRecordVO> signalShortLinkStatsRecord(SignalShortLinkStatsReqDTO signalShortLinkStats, BasePageReqDTO page) {
        String domain = signalShortLinkStats.getDomain();
        String shortUri = signalShortLinkStats.getShortUri();
        verifyShortLink(domain, shortUri);
        verifyDateSelector(signalShortLinkStats);
        if (page.getCurrent() < 0) {
            page.setDefaultCurrent();
        }
        if (page.getSize() < 0) {
            page.setDefaultSize();
        }

        LocalDate startDate = signalShortLinkStats.getStartDate();
        LocalDate endDate = signalShortLinkStats.getEndDate();
        Page<ShortLinkStatsRecordDO> convertPage = new Page<>();
        convertPage.setSize(page.getSize());
        convertPage.setSize(page.getCurrent());
        convertPage = shortLinkStatsRecordService.lambdaQuery()
                .eq(ShortLinkStatsRecordDO::getDomain, domain)
                .eq(ShortLinkStatsRecordDO::getShortUri, shortUri)
                .between(ShortLinkStatsRecordDO::getVisitDate, startDate, endDate)
                .eq(ShortLinkStatsRecordDO::getDelFlag, ShortLinkStatsRecordDO.DEL_FLAG_FAIL)
                .page(convertPage);

        if (ObjectUtil.isEmpty(convertPage.getRecords()) && convertPage.getCurrent() > 1) {
            long pageCount = (convertPage.getTotal() - 1) / convertPage.getSize() + 1;
            log.warn(String.format("pageCount(%s) current(%s) 页数超出", pageCount, convertPage.getCurrent()));
            page.setCurrent(pageCount);
            return signalShortLinkStatsRecord(signalShortLinkStats, page);
        }

        return shortStatsRecordConvertVO(convertPage);
    }

    @Override
    public ShortLinkStatsVO batchShortLinkStats(BatchShortLinkStatsReqDTO batchShortLinkStatsReqDTO) {
        List<ShortLinkReqDTO> shortLinks = batchShortLinkStatsReqDTO.getShortLinks();
        if (ObjectUtil.isEmpty(shortLinks)) {
            return null;
        }
        shortLinks.forEach(shortLink -> verifyShortLink(shortLink.getDomain(), shortLink.getShortUri()));
        verifyDateSelector(batchShortLinkStatsReqDTO);

        LocalDate startDate = batchShortLinkStatsReqDTO.getStartDate();
        LocalDate endDate = batchShortLinkStatsReqDTO.getEndDate();
        List<ShortLinkStatsRecordDO> list = shortLinkStatsRecordService.query()
                .inSql(SHORT_LINK_NAME, getShortLinkInSqlValue(shortLinks))
                .between(ShortLinkStatsRecordDO.VISIT_DATE_NAME, startDate, endDate)
                .eq(ShortLinkStatsRecordDO.DEL_FLAG_NAME, ShortLinkStatsRecordDO.DEL_FLAG_FAIL)
                .list();

        return stats(list, startDate, endDate);
    }

    @Override
    public IPage<ShortLinkStatsRecordVO> batchShortLinkStatsRecord(BatchShortLinkStatsReqDTO batchShortLinkStatsReqDTO, BasePageReqDTO page) {
        List<ShortLinkReqDTO> shortLinks = batchShortLinkStatsReqDTO.getShortLinks();
        if (ObjectUtil.isEmpty(shortLinks)) {
            return null;
        }
        shortLinks.forEach(shortLink -> verifyShortLink(shortLink.getDomain(), shortLink.getShortUri()));
        verifyDateSelector(batchShortLinkStatsReqDTO);
        if (page.getCurrent() < 0) {
            page.setDefaultCurrent();
        }
        if (page.getSize() < 0) {
            page.setDefaultSize();
        }

        Page<ShortLinkStatsRecordDO> convertPage = new Page<>();
        convertPage.setSize(page.getSize());
        convertPage.setSize(page.getCurrent());
        LocalDate startDate = batchShortLinkStatsReqDTO.getStartDate();
        LocalDate endDate = batchShortLinkStatsReqDTO.getEndDate();
        convertPage = shortLinkStatsRecordService.query()
                .inSql(SHORT_LINK_NAME, getShortLinkInSqlValue(shortLinks))
                .between(ShortLinkStatsRecordDO.VISIT_DATE_NAME, startDate, endDate)
                .eq(ShortLinkStatsRecordDO.DEL_FLAG_NAME, ShortLinkStatsRecordDO.DEL_FLAG_FAIL)
                .page(convertPage);

        if (ObjectUtil.isEmpty(convertPage.getRecords()) && convertPage.getCurrent() > 1) {
            long pageCount = (convertPage.getTotal() - 1) / convertPage.getSize() + 1;
            log.warn(String.format("pageCount(%s) current(%s) 页数超出", pageCount, convertPage.getCurrent()));
            page.setCurrent(pageCount);
            return batchShortLinkStatsRecord(batchShortLinkStatsReqDTO, page);
        }

        return shortStatsRecordConvertVO(convertPage);
    }

    private String getShortLinkInSqlValue(List<ShortLinkReqDTO> shortLinks) {
        if (ObjectUtil.isEmpty(shortLinks)) {
            return "";
        }

        ArrayList<String> shortLinkStrList = new ArrayList<>(shortLinks.size());
        shortLinks.forEach(shortLink -> shortLinkStrList.add(String.format("('%s', %s)", shortLink.getDomain(), shortLink.getShortUri())));
        return String.join(", ", shortLinkStrList);
    }

    private void verifyShortLink(String domain, String shortUri) {
        if (!ObjectUtil.isAllNotEmpty(domain, shortUri)) {
            log.error(String.format("domain(%s) shortUri(%s) 不能为空", domain, shortUri));
            throw new ClientException("参数错误");
        }

        if (!UrlUtils.isValidDomain(domain)) {
            log.error(String.format("domain(%s) 域名格式错误", domain));
            throw new ClientException("域名格式错误");
        }

        if (!UrlUtils.isValidShortUri(shortUri)) {
            log.error(String.format("shortUri(%s) 短链接格式错误", shortUri));
            throw new ClientException("短链接格式错误");
        }
    }

    private void verifyDateSelector(DateSelectorDTO dateSelector) {
        LocalDate startDate = dateSelector.getStartDate();
        LocalDate endDate = dateSelector.getEndDate();
        if (ObjectUtil.isEmpty(endDate) || endDate.isAfter(LocalDate.now())) {
            dateSelector.setEndDate(LocalDate.now());
        }
        if (ObjectUtil.isEmpty(startDate)) {
            dateSelector.setStartDate(endDate.minusDays(30));
        }
        if (startDate.isAfter(endDate)) {
            dateSelector.setStartDate(endDate);
        }
    }

    private ShortLinkStatsVO stats(List<ShortLinkStatsRecordDO> shortLinkStatsRecordDOList,
                                   LocalDate startDate,
                                   LocalDate endDate) {
        Integer pv = shortLinkStatsRecordDOList.size();
        AtomicReference<Integer> uv = new AtomicReference<>(0);
        AtomicReference<Integer> ip = new AtomicReference<>(0);
        HashMap<LocalDate, ShortLinkBaseDateStatsRespDTO> baseDateStats =
                new HashMap<>();
        for (LocalDate i = startDate; !i.isAfter(endDate); i = i.plusDays(1)) {
            ShortLinkBaseDateStatsRespDTO baseData = new ShortLinkBaseDateStatsRespDTO(i, 0, 0, 0);
            baseDateStats.put(i, baseData);
        }
        HashMap<String, ShortLinkLocaleStatsRespDTO> localeStats = new HashMap<>();
        int[] hourStats = new int[24];
        int[] weekStats = new int[7];
        HashMap<String, ShortLinkTopIpStatsRespDTO> ipStats = new HashMap<>();
        HashMap<String, ShortLinkOsStatsRespDTO> osStats = new HashMap<>();
        HashMap<String, ShortLinkBrowserStatsRespDTO> browserStats = new HashMap<>();
        HashMap<String, ShortLinkUvStatsRespDTO> uvTypeStats = new HashMap<>();
        HashMap<String, ShortLinkNetworkStatsRespDTO> netWorkStats = new HashMap<>();
        HashMap<String, ShortLinkDeviceStatsRespDTO> deviceStats = new HashMap<>();

        shortLinkStatsRecordDOList.forEach(record -> {
            ShortLinkBaseDateStatsRespDTO baseData = baseDateStats.get(record.getVisitDate());
            baseData.setPv(baseData.getPv() + 1);
            if (record.getUvType() == 0) {
                uv.getAndSet(uv.get() + 1);
                baseData.setUv(baseData.getUv() + 1);
            }
            if (record.getIpType() == 0) {
                ip.getAndSet(ip.get() + 1);
                baseData.setIp(baseData.getIp() + 1);
            }

            String locale = record.getLocale();
            ShortLinkLocaleStatsRespDTO localData = localeStats.get(locale);
            if (ObjectUtil.isEmpty(localData)) {
                localData = new ShortLinkLocaleStatsRespDTO();
                localData.setLocale(locale);
                localData.setNumber(0);
                localeStats.put(locale, localData);
            }
            localData.setNumber(localData.getNumber() + 1);

            int hour = record.getCreateTime().getHour();
            hourStats[hour]++;

            ShortLinkTopIpStatsRespDTO ipData = ipStats.get(record.getIp());
            if (ObjectUtil.isEmpty(ipData)) {
                ipData = new ShortLinkTopIpStatsRespDTO();
                ipData.setIp(record.getIp());
                ipData.setNumber(0);
                ipStats.put(record.getIp(), ipData);
            }
            ipData.setNumber(ipData.getNumber() + 1);

            int week = record.getCreateTime().getDayOfWeek().getValue() - 1;
            weekStats[week]++;

            String os = record.getOs();
            ShortLinkOsStatsRespDTO osData = osStats.get(os);
            if (ObjectUtil.isEmpty(osData)) {
                osData = new ShortLinkOsStatsRespDTO();
                osData.setOs(os);
                osData.setNumber(0);
                osStats.put(os, osData);
            }
            osData.setNumber(osData.getNumber() + 1);

            String browser = record.getBrowser();
            ShortLinkBrowserStatsRespDTO browserData = browserStats.get(browser);
            if (ObjectUtil.isEmpty(browserData)) {
                browserData = new ShortLinkBrowserStatsRespDTO();
                browserData.setBrowser(browser);
                browserData.setNumber(0);
                browserStats.put(browser, browserData);
            }
            browserData.setNumber(browserData.getNumber() + 1);

            String uvType = record.getUvType() == 0 ? "newUser" : "oldUser";
            ShortLinkUvStatsRespDTO uvData = uvTypeStats.get(uvType);
            if (ObjectUtil.isEmpty(uvData)) {
                uvData = new ShortLinkUvStatsRespDTO();
                uvData.setUvType(uvType);
                uvData.setNumber(0);
                uvTypeStats.put(uvType, uvData);
            }
            uvData.setNumber(uvData.getNumber() + 1);

            String network = record.getNetwork();
            ShortLinkNetworkStatsRespDTO networkData = netWorkStats.get(network);
            if (ObjectUtil.isEmpty(networkData)) {
                networkData = new ShortLinkNetworkStatsRespDTO();
                networkData.setNetwork(network);
                networkData.setNumber(0);
                netWorkStats.put(network, networkData);
            }
            networkData.setNumber(networkData.getNumber() + 1);

            String device = record.getDevice();
            ShortLinkDeviceStatsRespDTO deviceData = deviceStats.get(device);
            if (ObjectUtil.isEmpty(deviceData)) {
                deviceData = new ShortLinkDeviceStatsRespDTO();
                deviceData.setDevice(device);
                deviceData.setNumber(0);
                deviceStats.put(device, deviceData);
            }
            deviceData.setNumber(deviceData.getNumber() + 1);
        });

        for (ShortLinkLocaleStatsRespDTO localData : localeStats.values()) {
            localData.setRatio(Math.round(localData.getNumber() / (double) pv * 1000) / 1000.0);
        }

        List<ShortLinkTopIpStatsRespDTO> topIpStats = ipStats.values().stream()
                .sorted(Comparator.comparingInt(ShortLinkTopIpStatsRespDTO::getNumber).reversed())
                .limit(5).toList();

        for (ShortLinkOsStatsRespDTO osData : osStats.values()) {
            osData.setRatio(Math.round(osData.getNumber() / (double) pv * 1000) / 1000.0);
        }

        for (ShortLinkBrowserStatsRespDTO browserData : browserStats.values()) {
            browserData.setRatio(Math.round(browserData.getNumber() / (double) pv * 1000) / 1000.0);
        }

        for (ShortLinkUvStatsRespDTO uvData : uvTypeStats.values()) {
            uvData.setRatio(Math.round(uvData.getNumber() / (double) pv * 1000) / 1000.0);
        }

        for (ShortLinkNetworkStatsRespDTO networkData : netWorkStats.values()) {
            networkData.setRatio(Math.round(networkData.getNumber() / (double) pv * 1000) / 1000.0);
        }

        for (ShortLinkDeviceStatsRespDTO deviceData : deviceStats.values()) {
            deviceData.setRatio(Math.round(deviceData.getNumber() / (double) pv * 1000) / 1000.0);
        }

        return ShortLinkStatsVO.builder()
                .totalPv(pv)
                .totalUv(uv.get())
                .totalIp(ip.get())
                .baseDayStats(baseDateStats.values().stream().toList())
                .localeStats(localeStats.values().stream().toList())
                .hourStats(ListUtil.toList(Arrays.stream(hourStats).iterator()))
                .topIpStats(topIpStats)
                .weekStats(ListUtil.toList(Arrays.stream(weekStats).iterator()))
                .osStats(osStats.values().stream().toList())
                .browserStats(browserStats.values().stream().toList())
                .uvTypeStats(uvTypeStats.values().stream().toList())
                .networkStats(netWorkStats.values().stream().toList())
                .deviceStats(deviceStats.values().stream().toList())
                .build();
    }

    private IPage<ShortLinkStatsRecordVO> shortStatsRecordConvertVO(IPage<ShortLinkStatsRecordDO> dataPage) {
        Set<String> newUvIds = dataPage.getRecords()
                .stream()
                .filter(record -> record.getUvType() == 0)
                .map(ShortLinkStatsRecordDO::getUvId)
                .collect(Collectors.toSet());

        return dataPage.convert(recordDO -> {
            ShortLinkStatsRecordVO recordVO = BeanUtil.copyProperties(recordDO, ShortLinkStatsRecordVO.class);
            recordVO.setUvType(newUvIds.contains(recordDO.getUvId()) ? "新访客" : "旧访客");
            return recordVO;
        });
    }
}
